package ckw.node;

import java.util.List;

import javassist.tools.rmi.ObjectNotFoundException;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import ckw.communication.CommunicationService;
import ckw.database.DatabaseService;
import ckw.database.model.Node;
import ckw.database.model.NodeChanges;
import ckw.gui.MainWindow;
import core.communication.message.CNodeState;
import core.communication.message.Direction;
import core.communication.message.NodeStateEnum;
import core.database.NodeType;
import core.exceptions.ObjectDeleteException;

@Service
public class NodesServiceImpl implements NodesService, ApplicationContextAware {

	private static final Logger L = LoggerFactory.getLogger(NodesServiceImpl.class);

	private ApplicationContext context;
	private DatabaseService databaseService;
	private MainWindow mainWindow;

	@Inject
	public NodesServiceImpl(DatabaseService databaseService, MainWindow mainWindow) {
		L.debug("Tworzę NodesService");

		Assert.notNull(databaseService);
		Assert.notNull(mainWindow);

		this.databaseService = databaseService;
		this.mainWindow = mainWindow;

		L.debug("Utworzono NodesService");
	}

	@Override
	public Node getMainNode() {
		return databaseService.getMainNode();
	}

	@Override
	public Node addNode(Node node) {
		L.debug("Dodaję węzeł {}", node);

		if (node.getNodeType() == NodeType.CKW) {
			node.setParent(databaseService.getBaseNode());
		}

		if (node.getLevel() == 1)
			getCommunicationService().addClient(node);

		node = databaseService.storeNode(node);

		if (node.getLevel() > 0) {
			NodeChanges change = new NodeChanges(node.getLocatorURI());

			change.addActionSetParent(node.getParent().getLocatorURI());

			changeNodeUpToDate(node.getLocatorURI(), false);

			// if (node.getLevel() > 1) {
			updateNodesChangeOnAddNode(change, node, databaseService.getAllNodesByLevel(node.getLevel() - 1), Direction.UP);
			// }
			if (node.getNodeType() == NodeType.OKW)
				updateNodesChangeOnAddNode(change, node, databaseService.getAllNodesByLevel(node.getLevel() + 1), Direction.DOWN);

			databaseService.storeNodeChanges(change);
		}
		return node;
	}

	private void updateNodesChangeOnAddNode(NodeChanges change, Node addedNode, List<Node> nodesLayer, Direction direction) {

		if (nodesLayer != null) {
			for (Node node : nodesLayer) {
				if (direction == Direction.UP && node.getNodeType() == NodeType.RKW)
					continue;

				change.addActionAdd(node, direction);

				if (node.getNodeType() == NodeType.CKW)
					continue;

				NodeChanges newChange = databaseService.getNodeChanges(node.getLocatorURI());
				if (newChange == null)
					newChange = new NodeChanges(node.getLocatorURI());

				newChange.addActionAdd(addedNode, Direction.getOppositeDirection(direction));
				databaseService.storeNodeChanges(newChange);

				changeNodeUpToDate(node.getLocatorURI(), false);
			}
		}
	}

	@Override
	public void deleteNode(Node node) throws ObjectDeleteException {
		databaseService.deleteNode(node);

		L.debug("Usuwam węzeł {}", node);

		if (node.getLevel() == 1)
			getCommunicationService().deleteClient(node.getLocatorURI());

		if (node.getLevel() > 1) {
			updateNodesChangeOnDeleteNode(node.getLocatorURI(), databaseService.getAllNodesByLevel(node.getLevel() - 1));
		}
		updateNodesChangeOnDeleteNode(node.getLocatorURI(), databaseService.getAllNodesByLevel(node.getLevel() + 1));
	}

	private void updateNodesChangeOnDeleteNode(String locatorUri, List<Node> nodesLayer) {
		if (nodesLayer != null) {
			for (Node node : nodesLayer) {
				NodeChanges newChange = databaseService.getNodeChanges(node.getLocatorURI());
				if (newChange == null)
					newChange = new NodeChanges(node.getLocatorURI());

				newChange.addActionDelete(locatorUri);
				databaseService.storeNodeChanges(newChange);

				changeNodeUpToDate(node.getLocatorURI(), false);
			}
		}
	}

	@Override
	public void changeNodeState(String locatorURI, CNodeState state) {
		L.debug("Uaktualniam stan węzła {} na {}", locatorURI, state.getStateEnum());

		Node node = databaseService.getNode(locatorURI);

		if (node != null) {
			if (node.getState().getModifiedDate() != null)
				if (node.getState().getModifiedDate().compareTo(state.getModifiedDate()) >= 0) {
					L.debug("Stan nieaktualny");
					return;
				}
			node.changeState(state.getStateEnum());

			databaseService.storeNode(node);
			mainWindow.refreshCommunicationNode(node);

			if (state.getStateEnum() == NodeStateEnum.OFF)
				checkNodesReachability(node.getLevel());
			else if (state.getStateEnum() == NodeStateEnum.ON)
				changeNodesState(node.getLevel() + 1, NodeStateEnum.ON);

		} else {
			try {
				throw new ObjectNotFoundException("Nie znaleziono węzła");
			} catch (ObjectNotFoundException e) {
				e.printStackTrace();
			}
		}
	}

	private void checkNodesReachability(int level) {
		L.debug("Sprawdzam dostępność węzłów na poziomie {}", level);

		Boolean reachable = false;
		for (Node node : databaseService.getAllNodesByLevel(level)) {
			if (node.getNodeType() == NodeType.OKW && node.getState().getStateEnum() == NodeStateEnum.ON)
				reachable = true;
		}

		if (!reachable)
			changeNodesState(level + 1, NodeStateEnum.UNREACHABLE);
	}

	private void changeNodesState(int level, NodeStateEnum state) {
		L.debug("Zmieniam stan węzłów poziomu {} na {}", level, state);

		List<Node> nodes = databaseService.getAllNodesByLevel(level);
		if (!nodes.isEmpty()) {
			for (Node node : nodes) {
				if (node.getState().getStateEnum() == NodeStateEnum.ON) {
					node.changeState(state);

					databaseService.storeNode(node);
					mainWindow.refreshCommunicationNode(node);
				}
			}
			changeNodesState(level + 1, state);
		}
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}

	@Override
	public Node getNode(String locatorURI) {
		return databaseService.getNode(locatorURI);
	}

	@Override
	public Node updateNode(Node node) {
		Node oldNode = databaseService.getNode(node.getId());

		String uri = node.getLocatorURI();

		if (oldNode == null)
			try {
				throw new ObjectNotFoundException("Nie znaleziono węzła");
			} catch (ObjectNotFoundException e) {
				e.printStackTrace();
			}

		if (!node.getLocatorURI().equals(oldNode.getLocatorURI())) {
			uri = oldNode.getLocatorURI();

			NodeChanges change = databaseService.getNodeChanges(oldNode.getLocatorURI());
			if (change != null) {
				change.setUri(node.getLocatorURI());
				databaseService.storeNodeChanges(change);
			}

			if (oldNode.getLevel() == 1)
				getCommunicationService().changeClient(oldNode.getLocatorURI(), node);
		}

		if (node.getLevel() > 1) {
			updateNodesChangeOnUpdateNode(uri, node, databaseService.getAllNodesByLevel(node.getLevel() - 1), Direction.DOWN);
		}
		updateNodesChangeOnUpdateNode(uri, node, databaseService.getAllNodesByLevel(node.getLevel() + 1), Direction.UP);

		return databaseService.storeNode(node);
	}

	private void updateNodesChangeOnUpdateNode(String oldNodeUri, Node updatedNode, List<Node> nodesLayer, Direction direction) {

		if (nodesLayer != null) {
			for (Node node : nodesLayer) {
				NodeChanges newChange = databaseService.getNodeChanges(node.getLocatorURI());
				if (newChange == null)
					newChange = new NodeChanges(node.getLocatorURI());

				newChange.addActionEdit(updatedNode, oldNodeUri, direction);
				databaseService.storeNodeChanges(newChange);

				changeNodeUpToDate(node.getLocatorURI(), false);
			}
		}
	}

	private CommunicationService getCommunicationService() {
		return context.getBean(CommunicationService.class);
	}

	@Override
	public void changeNodeUpToDate(String nodeUri, boolean upToDate) {
		L.debug("Uaktualniam stan węzła {} na {}", nodeUri, upToDate);

		Node node = databaseService.getNode(nodeUri);
		node.setUpToDate(upToDate);
		node = databaseService.storeNode(node);
		mainWindow.refreshCommunicationNode(node);
	}
}
